/*
Copyright 2022 The Kruise Authors.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package webhook

import (
	"context"
	"flag"
	"fmt"

	admissionregistrationv1 "k8s.io/api/admissionregistration/v1"
	"k8s.io/apimachinery/pkg/api/errors"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/runtime"
	clientset "k8s.io/client-go/kubernetes"
	"k8s.io/client-go/rest"
	"sigs.k8s.io/controller-runtime/pkg/manager"
	"sigs.k8s.io/controller-runtime/pkg/webhook"
	"sigs.k8s.io/controller-runtime/pkg/webhook/admission"

	gamekruiseiov1alpha1 "github.com/openkruise/kruise-game/apis/v1alpha1"
	manager2 "github.com/openkruise/kruise-game/cloudprovider/manager"
	"github.com/openkruise/kruise-game/pkg/webhook/util/generator"
	"github.com/openkruise/kruise-game/pkg/webhook/util/writer"
)

var (
	mutatePodPath                      = "/mutate-v1-pod"
	validateGssPath                    = "/validate-v1alpha1-gss"
	mutatingWebhookConfigurationName   = "kruise-game-mutating-webhook"
	validatingWebhookConfigurationName = "kruise-game-validating-webhook"
)

var (
	webhookPort             int
	webhookCertDir          string
	webhookServiceNamespace string
	webhookServiceName      string
	enableCertGeneration    bool
)

func init() {
	flag.IntVar(&webhookPort, "webhook-port", 9876, "The port of the MutatingWebhookConfiguration object.")
	flag.StringVar(&webhookCertDir, "webhook-server-certs-dir", "/tmp/webhook-certs/", "Path to the X.509-formatted webhook certificate.")
	flag.StringVar(&webhookServiceNamespace, "webhook-service-namespace", "kruise-game-system", "kruise game webhook service namespace.")
	flag.StringVar(&webhookServiceName, "webhook-service-name", "kruise-game-webhook-service", "kruise game wehook service name.")
	flag.BoolVar(&enableCertGeneration, "enable-cert-generation", true, "Whether to enable self-generated certs for webhook server. If set to false, you need to provide the certs in the specified directory.")
}

// +kubebuilder:rbac:groups=apps.kruise.io,resources=statefulsets,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups=apps.kruise.io,resources=statefulsets/status,verbs=get;update;patch
// +kubebuilder:rbac:groups=apps.kruise.io,resources=podprobemarkers,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups=core,resources=pods,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups=core,resources=pods/status,verbs=get;update;patch
// +kubebuilder:rbac:groups=core,resources=services,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups=core,resources=services/status,verbs=get;update;patch
// +kubebuilder:rbac:groups=networking.k8s.io,resources=ingresses,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups=networking.k8s.io,resources=ingresses/status,verbs=get;update;patch
// +kubebuilder:rbac:groups=core,resources=nodes,verbs=get;list;watch
// +kubebuilder:rbac:groups=core,resources=nodes/status,verbs=get
// +kubebuilder:rbac:groups=core,resources=persistentvolumeclaims,verbs=get;list;watch
// +kubebuilder:rbac:groups=core,resources=persistentvolumeclaims/status,verbs=get
// +kubebuilder:rbac:groups=core,resources=persistentvolumes,verbs=get;list;watch
// +kubebuilder:rbac:groups=core,resources=persistentvolumes/status,verbs=get
// +kubebuilder:rbac:groups=admissionregistration.k8s.io,resources=mutatingwebhookconfigurations,verbs=create;get;list;watch;update;patch
// +kubebuilder:rbac:groups=admissionregistration.k8s.io,resources=validatingwebhookconfigurations,verbs=create;get;list;watch;update;patch
// +kubebuilder:rbac:groups=apiextensions.k8s.io,resources=customresourcedefinitions,verbs=get;list;watch;update;patch
// +kubebuilder:rbac:groups=alibabacloud.com,resources=poddnats,verbs=get;list;watch
// +kubebuilder:rbac:groups=alibabacloud.com,resources=poddnats/status,verbs=get
// +kubebuilder:rbac:groups=alibabacloud.com,resources=podeips,verbs=get;list;watch
// +kubebuilder:rbac:groups=alibabacloud.com,resources=podeips/status,verbs=get
// +kubebuilder:rbac:groups="",resources=events,verbs=create;patch
// +kubebuilder:rbac:groups=elbv2.k8s.aws,resources=targetgroupbindings,verbs=create;get;list;patch;update;watch
// +kubebuilder:rbac:groups=elbv2.services.k8s.aws,resources=listeners,verbs=create;get;list;patch;update;watch
// +kubebuilder:rbac:groups=elbv2.services.k8s.aws,resources=targetgroups,verbs=create;get;list;patch;update;watch
// +kubebuilder:rbac:groups=networking.cloud.tencent.com,resources=dedicatedclblisteners,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups=networking.cloud.tencent.com,resources=dedicatedclblisteners/status,verbs=get

type Webhook struct {
	mgr manager.Manager
	cpm *manager2.ProviderManager
}

func NewWebhookServer(mgr manager.Manager, cpm *manager2.ProviderManager) *Webhook {
	return &Webhook{
		mgr: mgr,
		cpm: cpm,
	}
}

func (ws *Webhook) SetupWithManager(mgr manager.Manager) *Webhook {
	server := mgr.GetWebhookServer()
	decoder := admission.NewDecoder(runtime.NewScheme())

	// Register the webhook server
	recorder := mgr.GetEventRecorderFor("kruise-game-webhook")
	server.Register(mutatePodPath, &webhook.Admission{Handler: NewPodMutatingHandler(mgr.GetClient(), decoder, ws.cpm, recorder)})
	server.Register(validateGssPath, &webhook.Admission{Handler: &GssValidaatingHandler{Client: mgr.GetClient(), decoder: decoder, CloudProviderManager: ws.cpm}})
	return ws
}

// Initialize create MutatingWebhookConfiguration before start
func (ws *Webhook) Initialize(cfg *rest.Config) error {
	if enableCertGeneration {
		dnsName := generator.ServiceToCommonName(webhookServiceNamespace, webhookServiceName)
		// if enable self-generated certs, ensure the certs are generated and written to the specified directory
		if webhookCertDir == "" {
			return fmt.Errorf("webhook cert dir is not set")
		}
		var err error
		var certWriter writer.CertWriter
		certWriter, err = writer.NewFSCertWriter(writer.FSCertWriterOptions{Path: webhookCertDir})
		if err != nil {
			return fmt.Errorf("failed to constructs FSCertWriter: %v", err)
		}

		certs, _, err := certWriter.EnsureCert(dnsName)
		if err != nil {
			return fmt.Errorf("failed to ensure certs: %v", err)
		}

		if err := writer.WriteCertsToDir(webhookCertDir, certs); err != nil {
			return fmt.Errorf("failed to write certs to dir: %v", err)
		}

		clientSet, err := clientset.NewForConfig(cfg)
		if err != nil {
			return err
		}

		if err := checkValidatingConfiguration(clientSet, certs.CACert); err != nil {
			return fmt.Errorf("failed to check mutating webhook,because of %s", err.Error())
		}

		if err := checkMutatingConfiguration(clientSet, certs.CACert); err != nil {
			return fmt.Errorf("failed to check mutating webhook,because of %s", err.Error())
		}
	}

	return nil
}

func checkValidatingConfiguration(kubeClient clientset.Interface, caBundle []byte) error {
	vwc, err := kubeClient.AdmissionregistrationV1().ValidatingWebhookConfigurations().Get(context.TODO(), validatingWebhookConfigurationName, metav1.GetOptions{})
	if err != nil {
		if errors.IsNotFound(err) {
			// create new webhook
			return createValidatingWebhook(kubeClient, caBundle)
		} else {
			return err
		}
	}
	return updateValidatingWebhook(vwc, kubeClient, caBundle)
}

func checkMutatingConfiguration(kubeClient clientset.Interface, caBundle []byte) error {
	mwc, err := kubeClient.AdmissionregistrationV1().MutatingWebhookConfigurations().Get(context.TODO(), mutatingWebhookConfigurationName, metav1.GetOptions{})
	if err != nil {
		if errors.IsNotFound(err) {
			// create new webhook
			return createMutatingWebhook(kubeClient, caBundle)
		} else {
			return err
		}
	}
	return updateMutatingWebhook(mwc, kubeClient, caBundle)
}

func createValidatingWebhook(kubeClient clientset.Interface, caBundle []byte) error {
	webhookConfig := &admissionregistrationv1.ValidatingWebhookConfiguration{
		ObjectMeta: metav1.ObjectMeta{
			Name: validatingWebhookConfigurationName,
		},
		Webhooks: getValidatingWebhookConf(caBundle),
	}

	if _, err := kubeClient.AdmissionregistrationV1().ValidatingWebhookConfigurations().Create(context.TODO(), webhookConfig, metav1.CreateOptions{}); err != nil {
		return fmt.Errorf("failed to create %s: %v", validatingWebhookConfigurationName, err)
	}
	return nil
}

func createMutatingWebhook(kubeClient clientset.Interface, caBundle []byte) error {
	webhookConfig := &admissionregistrationv1.MutatingWebhookConfiguration{
		ObjectMeta: metav1.ObjectMeta{
			Name: mutatingWebhookConfigurationName,
		},
		Webhooks: getMutatingWebhookConf(caBundle),
	}

	if _, err := kubeClient.AdmissionregistrationV1().MutatingWebhookConfigurations().Create(context.TODO(), webhookConfig, metav1.CreateOptions{}); err != nil {
		return fmt.Errorf("failed to create %s: %v", mutatingWebhookConfigurationName, err)
	}
	return nil
}

func updateValidatingWebhook(vwc *admissionregistrationv1.ValidatingWebhookConfiguration, kubeClient clientset.Interface, caBundle []byte) error {
	vwc.Webhooks = getValidatingWebhookConf(caBundle)
	if _, err := kubeClient.AdmissionregistrationV1().ValidatingWebhookConfigurations().Update(context.TODO(), vwc, metav1.UpdateOptions{}); err != nil {
		return fmt.Errorf("failed to update %s: %v", validatingWebhookConfigurationName, err)
	}
	return nil
}

func updateMutatingWebhook(mwc *admissionregistrationv1.MutatingWebhookConfiguration, kubeClient clientset.Interface, caBundle []byte) error {
	mwc.Webhooks = getMutatingWebhookConf(caBundle)
	if _, err := kubeClient.AdmissionregistrationV1().MutatingWebhookConfigurations().Update(context.TODO(), mwc, metav1.UpdateOptions{}); err != nil {
		return fmt.Errorf("failed to update %s: %v", mutatingWebhookConfigurationName, err)
	}
	return nil
}

func getValidatingWebhookConf(caBundle []byte) []admissionregistrationv1.ValidatingWebhook {
	sideEffectClassNone := admissionregistrationv1.SideEffectClassNone
	fail := admissionregistrationv1.Fail
	return []admissionregistrationv1.ValidatingWebhook{
		{
			Name:                    "vgameserverset.kb.io",
			SideEffects:             &sideEffectClassNone,
			FailurePolicy:           &fail,
			AdmissionReviewVersions: []string{"v1", "v1beta1"},
			ClientConfig: admissionregistrationv1.WebhookClientConfig{
				Service: &admissionregistrationv1.ServiceReference{
					Namespace: webhookServiceNamespace,
					Name:      webhookServiceName,
					Path:      &validateGssPath,
				},
				CABundle: caBundle,
			},
			Rules: []admissionregistrationv1.RuleWithOperations{
				{
					Operations: []admissionregistrationv1.OperationType{admissionregistrationv1.Create, admissionregistrationv1.Update},
					Rule: admissionregistrationv1.Rule{
						APIGroups:   []string{"game.kruise.io"},
						APIVersions: []string{"v1alpha1"},
						Resources:   []string{"gameserversets"},
					},
				},
			},
		},
	}
}

func getMutatingWebhookConf(caBundle []byte) []admissionregistrationv1.MutatingWebhook {
	sideEffectClassNone := admissionregistrationv1.SideEffectClassNone
	fail := admissionregistrationv1.Fail
	return []admissionregistrationv1.MutatingWebhook{
		{
			Name:                    "mgameserverset.kb.io",
			SideEffects:             &sideEffectClassNone,
			FailurePolicy:           &fail,
			AdmissionReviewVersions: []string{"v1", "v1beta1"},
			ClientConfig: admissionregistrationv1.WebhookClientConfig{
				Service: &admissionregistrationv1.ServiceReference{
					Namespace: webhookServiceNamespace,
					Name:      webhookServiceName,
					Path:      &mutatePodPath,
				},
				CABundle: caBundle,
			},
			Rules: []admissionregistrationv1.RuleWithOperations{
				{
					Operations: []admissionregistrationv1.OperationType{admissionregistrationv1.Create, admissionregistrationv1.Update, admissionregistrationv1.Delete},
					Rule: admissionregistrationv1.Rule{
						APIGroups:   []string{""},
						APIVersions: []string{"v1"},
						Resources:   []string{"pods"},
					},
				},
			},
			ObjectSelector: &metav1.LabelSelector{
				MatchExpressions: []metav1.LabelSelectorRequirement{
					{
						Key:      gamekruiseiov1alpha1.GameServerOwnerGssKey,
						Operator: metav1.LabelSelectorOpExists,
						Values:   []string{},
					},
				},
			},
		},
	}
}

func GetPort() int {
	return webhookPort
}

func GetCertDir() string {
	return webhookCertDir
}
